/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.volley.toolbox;

import android.os.SystemClock;

import com.android.volley.AuthFailureError;
import com.android.volley.Cache;
import com.android.volley.Cache.Entry;
import com.android.volley.Network;
import com.android.volley.NetworkError;
import com.android.volley.NetworkResponse;
import com.android.volley.NoConnectionError;
import com.android.volley.RedirectError;
import com.android.volley.Request;
import com.android.volley.RetryPolicy;
import com.android.volley.ServerError;
import com.android.volley.TimeoutError;
import com.android.volley.VolleyError;
import com.android.volley.VolleyLog;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.impl.cookie.DateUtils;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

/**
 * A network performing Volley requests over an {@link HttpStack}.
 */
public class BasicNetwork implements Network
{
	protected static final boolean DEBUG = VolleyLog.DEBUG;
	
	private static int SLOW_REQUEST_THRESHOLD_MS = 3000;
	
	private static int DEFAULT_POOL_SIZE = 4096;
	
	protected final HttpStack mHttpStack;
	
	protected final ByteArrayPool mPool;
	
	/**
	 * @param httpStack HTTP stack to be used
	 */
	public BasicNetwork( HttpStack httpStack )
	{
		// If a pool isn't passed in, then build a small default pool that will give us a lot of
		// benefit and not use too much memory.
		this( httpStack, new ByteArrayPool( DEFAULT_POOL_SIZE ) );
	}
	
	/**
	 * @param httpStack HTTP stack to be used
	 * @param pool a buffer pool that improves GC performance in copy operations
	 */
	public BasicNetwork(
			HttpStack httpStack,
			ByteArrayPool pool )
	{
		mHttpStack = httpStack;
		mPool = pool;
	}
	
	@Override
	public NetworkResponse performRequest( Request< ? > request ) throws VolleyError
	{
		long requestStart = SystemClock.elapsedRealtime();
		while( true )
		{
			HttpResponse httpResponse = null;
			byte[] responseContents = null;
			Map< String, String > responseHeaders = Collections.emptyMap();
			try
			{
				// Gather headers.
				Map< String, String > headers = new HashMap< String, String >();
				addCacheHeaders( headers, request.getCacheEntry() );
				httpResponse = mHttpStack.performRequest( request, headers );
				StatusLine statusLine = httpResponse.getStatusLine();
				int statusCode = statusLine.getStatusCode();
				
				responseHeaders = convertHeaders( httpResponse.getAllHeaders() );
				// Handle cache validation.
				if( statusCode == HttpStatus.SC_NOT_MODIFIED )
				{
					
					Entry entry = request.getCacheEntry();
					if( entry == null )
					{
						return new NetworkResponse( HttpStatus.SC_NOT_MODIFIED, null, responseHeaders, true, SystemClock.elapsedRealtime() - requestStart );
					}
					
					// A HTTP 304 response does not have all header fields. We
					// have to use the header fields from the cache entry plus
					// the new ones from the response.
					// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
					entry.responseHeaders.putAll( responseHeaders );
					return new NetworkResponse( HttpStatus.SC_NOT_MODIFIED, entry.data, entry.responseHeaders, true, SystemClock.elapsedRealtime() - requestStart );
				}
				
				// Handle moved resources
				if( statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY )
				{
					String newUrl = responseHeaders.get( "Location" );
					request.setRedirectUrl( newUrl );
				}
				
				// Some responses such as 204s do not have content.  We must check.
				if( httpResponse.getEntity() != null )
				{
					responseContents = entityToBytes( httpResponse.getEntity() );
				}
				else
				{
					// Add 0 byte response as a way of honestly representing a
					// no-content request.
					responseContents = new byte[ 0 ];
				}
				
				// if the request is slow, log it.
				long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
				logSlowRequests( requestLifetime, request, responseContents, statusLine );
				
				if( statusCode < 200 || statusCode > 299 )
				{
					throw new IOException();
				}
				return new NetworkResponse( statusCode, responseContents, responseHeaders, false, SystemClock.elapsedRealtime() - requestStart );
			}
			catch( SocketTimeoutException e )
			{
				attemptRetryOnException( "socket", request, new TimeoutError() );
			}
			catch( ConnectTimeoutException e )
			{
				attemptRetryOnException( "connection", request, new TimeoutError() );
			}
			catch( MalformedURLException e )
			{
				throw new RuntimeException( "Bad URL " + request.getUrl(), e );
			}
			catch( IOException e )
			{
				int statusCode = 0;
				NetworkResponse networkResponse = null;
				if( httpResponse != null )
				{
					statusCode = httpResponse.getStatusLine().getStatusCode();
				}
				else
				{
					throw new NoConnectionError( e );
				}
				if( statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY )
				{
					VolleyLog.e( "Request at %s has been redirected to %s", request.getOriginUrl(), request.getUrl() );
				}
				else
				{
					VolleyLog.e( "Unexpected response code %d for %s", statusCode, request.getUrl() );
				}
				if( responseContents != null )
				{
					networkResponse = new NetworkResponse( statusCode, responseContents, responseHeaders, false, SystemClock.elapsedRealtime() - requestStart );
					if( statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN )
					{
						attemptRetryOnException( "auth", request, new AuthFailureError( networkResponse ) );
					}
					else if( statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY )
					{
						attemptRetryOnException( "redirect", request, new RedirectError( networkResponse ) );
					}
					else
					{
						// TODO: Only throw ServerError for 5xx status codes.
						throw new ServerError( networkResponse );
					}
				}
				else
				{
					throw new NetworkError( e );
				}
			}
		}
	}
	
	/**
	 * Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete.
	 */
	private void logSlowRequests(
			long requestLifetime,
			Request< ? > request,
			byte[] responseContents,
			StatusLine statusLine )
	{
		if( DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS )
		{
			VolleyLog.d( "HTTP response for request=<%s> [lifetime=%d], [size=%s], " + "[rc=%d], [retryCount=%s]", request, requestLifetime, responseContents != null ? responseContents.length : "null", statusLine.getStatusCode(), request.getRetryPolicy().getCurrentRetryCount() );
		}
	}
	
	/**
	 * Attempts to prepare the request for a retry. If there are no more attempts remaining in the
	 * request's retry policy, a timeout exception is thrown.
	 * @param request The request to use.
	 */
	private static void attemptRetryOnException(
			String logPrefix,
			Request< ? > request,
			VolleyError exception ) throws VolleyError
	{
		RetryPolicy retryPolicy = request.getRetryPolicy();
		int oldTimeout = request.getTimeoutMs();
		
		try
		{
			retryPolicy.retry( exception );
		}
		catch( VolleyError e )
		{
			request.addMarker( String.format( "%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout ) );
			throw e;
		}
		request.addMarker( String.format( "%s-retry [timeout=%s]", logPrefix, oldTimeout ) );
	}
	
	private void addCacheHeaders(
			Map< String, String > headers,
			Cache.Entry entry )
	{
		// If there's no cache entry, we're done.
		if( entry == null )
		{
			return;
		}
		
		if( entry.etag != null )
		{
			headers.put( "If-None-Match", entry.etag );
		}
		
		if( entry.lastModified > 0 )
		{
			Date refTime = new Date( entry.lastModified );
			headers.put( "If-Modified-Since", DateUtils.formatDate( refTime ) );
		}
	}
	
	protected void logError(
			String what,
			String url,
			long start )
	{
		long now = SystemClock.elapsedRealtime();
		VolleyLog.v( "HTTP ERROR(%s) %d ms to fetch %s", what, ( now - start ), url );
	}
	
	/** Reads the contents of HttpEntity into a byte[]. */
	private byte[] entityToBytes( HttpEntity entity ) throws IOException, ServerError
	{
		PoolingByteArrayOutputStream bytes = new PoolingByteArrayOutputStream( mPool, ( int )entity.getContentLength() );
		byte[] buffer = null;
		try
		{
			InputStream in = entity.getContent();
			if( in == null )
			{
				throw new ServerError();
			}
			buffer = mPool.getBuf( 1024 );
			int count;
			while( ( count = in.read( buffer ) ) != -1 )
			{
				bytes.write( buffer, 0, count );
			}
			return bytes.toByteArray();
		}
		finally
		{
			try
			{
				// Close the InputStream and release the resources by "consuming the content".
				entity.consumeContent();
			}
			catch( IOException e )
			{
				// This can happen if there was an exception above that left the entity in
				// an invalid state.
				VolleyLog.v( "Error occured when calling consumingContent" );
			}
			mPool.returnBuf( buffer );
			bytes.close();
		}
	}
	
	/**
	 * Converts Headers[] to Map<String, String>.
	 */
	protected static Map< String, String > convertHeaders( Header[] headers )
	{
		Map< String, String > result = new TreeMap< String, String >( String.CASE_INSENSITIVE_ORDER );
		for( int i = 0 ; i < headers.length ; i++ )
		{
			result.put( headers[ i ].getName(), headers[ i ].getValue() );
		}
		return result;
	}
}
